programming perspectives

Adding topics to 11ty

Introduction

When I was starting my blog I quickly realised that I wanted to by able to group posts by topics, so that user could dive into specific areas of interest - rather than having to browse through everything. I couldn't find anything native in eleventy to do this for me, so I started working out how to do it. This post is a high level overview of how I approached it. I've simplified some areas, but hopefully this gives you some clues. The process of adding topics taught me a lot about how eleventy works and how powerful customisations can be - hopefully it helps you too! You can always find me on Mastadon if you have any questions

Note that I use nunjacks syntax, things will be different if you use Liquid or so on

Adding a topic to a post

We start by adding a topic property to the front matter of post markdown files. This makes it easy to assign a post to a topci

---
title: foo
topic: leadership
---

Happily, we don't need to do anything else to be able to add properties to front matter. You don't need to register the property in the eleventy config or anything else. We can then access this on the data property of the post object - I tend to run a null check as well (assuming a default value), just in case the post hasn't been assigned a topic yet

const topic = post.data?.topic || 'unclassified';

Adding topic header data

Now that posts have topics, we need to display the topics themselves somehow. To achieve this, let's start by adding some topic data. I added a new js file into the _data folder called topicdata.js, looking something like this:

module.exports = {
    leadership: {
        displayOrder: 1,
        title: "leadership",
        description: "Posts around leadership"
    },
    coding: {
        displayOrder: 2,
        title: "coding",
        description: "The coding topic"
    }
}

Just adding the file makes the data available throughout the rest of the site, with no additional configuration. For example, we access this in layout files like so:

{% set topics = topicdata | orderedTopicKeys %}

You'll notice here that we have referenced the data purely by name - we don't need to do anything else. You may also have noticed | orderedTopicKeys. That's a filter, we'll come back to that when we discuss filters in a moment

Listing topics

Now we can assign topics to posts, it would be nice to be able to let the user see them. We start by adding a file to list the topics, mine is called _includes/topiclist.njk

A simplified version looks like this:

<div class="topic-container">
{% set topics = topicdata | orderedTopicKeys %}

{% for topicKey in topics %}
    {% set topicValue = topicdata[topicKey] %}

    <div class="topic">
        <h2><a href="./topics/{{ topicKey }}/">{{ topicValue.title }}</a></h2>
        <section class="topic-description">
            {{ topicValue.description }}
        </section>
    </div>

{% endfor %}
</div>

Let's break down what's going on:

{% set topics = topicdata | orderedTopicKeys %}
{% for topicKey in topics %}
{% set topicValue = topicdata[topicKey] %}
    <div class="topic">
        <h2><a href="./topics/{{ topicKey }}/">{{ topicValue.title }}</a></h2>
        <section class="topic-description">
            {{ topicValue.description }}
        </section>
    </div>

Filtering topic data

I keep promising to dicuss the orderTopicKeys filter, so let's do it now. Filters are a powerful feature of eleventy and allow you to transform the data that you pass to them - even better, you can chain them together. To add filters we need to add a file to contain the filter. Mine is called eleventy.config.topics and looks something like this:

module.exports = eleventyConfig => {
    eleventyConfig.addFilter("orderedTopicKeys", function orderedTopicKeys(items) {
        const orderedTopics = [];

        // create a map of topic key and display order
        for (const key in items) {
                orderedTopics.push({ topic: key, displayOrder: items[key]?.displayOrder })
        };

        // order this by display order
        const topics = orderedTopics.sort((a, b) => {
                if (a.displayOrder < b.displayOrder) return -1;
                if (a.displayOrder > b.displayOrder) return 1;
                return 0;
        });

        // return the ordered topics
        return topics.map(t => t.topic);
    })
}

Then we need to register the filter in our eleventy configuration. Happily this is straightforward, you just update your eleventy.config.js as follows (note that you will almost certainly have a lot more config than this)

const pluginTopics = require("./eleventy.config.topics.js");

module.exports = function(eleventyConfig) {

  /* omitted for clarity */

	eleventyConfig.addPlugin(pluginTopics);

  /* omitted for clarity */
}

I said I'd explain how this all works, so let's get into that. There's quite a bit going on here, so let's break down how it hangs together:

eleventyConfig.addFilter("orderedTopicKeys", function orderedTopicKeys(items) {
  const topics = orderedTopics.sort((a, b) => {
          if (a.displayOrder < b.displayOrder) return -1;
          if (a.displayOrder > b.displayOrder) return 1;
          return 0;
  });

And that's it - fairly straight forward.

If you wanted to chain filters, you do it like this:

{% set foo = somedata | filter1 | filter2 %}

Where the output of filter1 is passed to filter2, and so on

Listing posts within a topic

Did you notice how I didn't mention anything about the link to the topics page, then I added the topiclist.njk template? This line:

<h2><a href="./topics//"></a></h2>

Obviously we need to generate a page per the topic to support this. We could just add these manually, of course, but wouldn't it be nicer if we could do it automatically based on our topicdata json? Well guess what, we can. I created a file called topic.njk in the content folder for this purpose. It looks something like this:

---
pagination:
  data: topicdata
  size: 1
  alias: topic
  addAllPagesToCollections: true
layout: layouts/base.njk
eleventyComputed:
  title: Topic “{{ topic }}”
permalink: /topics/{{ topic | slugify }}/
---
{% set topicData = topicdata[topic] %}
<h2>{{ topicData.title }}</h2>
<p>{{ topicData.description }}</p>

{% set postslist = collections.posts | filterPostsByTopic(topic) %} 
{% include "postslist.njk" %}

As usual, let's examine what's happening here, starting with the front matter. The key to really understanding this is to remember that eleventy is a static site generator; think of it like a compiler for your website that is going to 'compile' a page for each property of the topicdata

pagination:
  data: topicdata
  size: 1
  alias: topic
  addAllPagesToCollections: true
eleventyComputed:
  title: Topic “coding”
permalink: /topics/coding/

Having sorted out the front matter, let's look at the page content itself

Example value of topicData variable, taken from the from topicdata global data file (see the top of this post if you've forgotten this):

{
    displayOrder: 2,
    title: "coding",
    description: "Posts relating to coding - this covers all sorts of things relating to the land of curly brackets",
}

Example of how we acess and use this in the template:

{% set topicData = topicdata[topic] %}
<h2>{{ topicData.title }}</h2>
<p>{{ topicData.description }}</p>
{% set postslist = collections.posts | filterPostsByTopic(topic) %} 
{% include "postslist.njk" %}

Adding the posts

OK, so I may have glossed over the bit where we add the posts into the topic data file just now. There aren't any new concepts here, but let's cover it anyway:

We just need to add a template called 'postslist.njk', as follows:

<ul class="postlist">
{% for post in postslist | reverse %}
	<li class="postlist-item">
		<a href="{{ post.url }}" class="postlist-link">{% if post.data.title %}{{ post.data.title }}{% else %}{{ post.url }}{% endif %}</a></h2>
		<time class="postlist-date" datetime="{{ post.date | htmlDateString }}">{{ post.date | readableDate }}</time>
		<p>{{ post.data.description }}</p>
	</li>
{% endfor %}
</ul>

Notice that the statement {% for post in postslist | reverse %} reference the postslist variable that we set in the topiclist.njk template. Let's quickly look at how that bit works:

Template: topiclist.njk

<!-- set the post list variable -->
{% set postslist = collections.posts | filterPostsByTopic(topic) %} 

<!-- call the template -->
{% include "postslist.njk" %}

File: postslist.njk

<!-- we can use this variable in the template now -->
{% for post in postslist | reverse %}

You can also see a sneaky | reverse filter that is applied to the variable, to reverse the order of the posts

Everything after that is just us chosing the bits of the post we want to display

Adding topics to my homepage

The final piece of this is to show the list of topics in the index page - as I want that to be the default view for my site. We can do this by just reference the topiclist.njk in the index.njk, like this:

---
layout: layouts/home.njk
eleventyNavigation:
  key: topics
  order: 10
---
{% include "topiclist.njk" %}

References

I've skimmed over a lot of deeper topics here, I'd recommend having a read of the following if you want more details:

What next?

I don't have a comment section on my blog at the moment, but I'm always happy to chat on Mastadon